他の種類の関数を 2 トラックモデルに適合させる
LT;DR
ライブラリやサービスから発生する例外は、 ドメインエラー として扱いたい場合がある これを実現するには、例外を発生させる関数を Result に返す関数に変換する アダプタブロック を作成すれば良い https://scrapbox.io/files/66af67232e1e4d001c960451.png
何も返さない関数は入力値を返すようにし、Result.map を適用することで、2 トラックモデル に適合できる https://scrapbox.io/files/66b06547fdd4fb001c445b0e.png
hr.icon
ここでは以下の 2 種類の関数に絞ってみる
例外を投げる関数
何も返さない関数
例外を投げる関数
実装時に例外を投げるのを避けたとしても、ライブラリなど管理していないコードは例外を投げる
自分たちで投げた例外(パニック)は ドメインの一部ではないので、トップレベルでのみ捕捉する 一方、ライブラリなどの例外は ドメインエラー として扱いたいケースもある どう実現するか?
例外を発生させる関数を、Result を返す関数に変換する アダプタブロック を作成する https://scrapbox.io/files/66af67232e1e4d001c960451.png
例外を発生させる関数を、Result を返す関数に変換するアダプタブロックの例
外部サービスからのタイムアウトを捕捉し、RemoteServiceError に変換する
code:fsharp
type ServiceInfo = {
Name: string
Endpoint: Uri
}
type RemoteServiceError = {
Service: ServiceInfo
Exception: System.Exception
}
code:fsharp
let serviceExceptionAdapter serviceInfo serviceFn x =
try
Ok (serviceFn x) // サービスを呼び出して成功を返す
with
| :? TimeoutException as ex ->
Error { Service = serviceInfo; Exception = ex }
| :? AuthorizationExcetion as ex ->
Error { Service = serviceInfo; Exception = ex }
注目すべきは、ドメインに関連するものだけを捕捉している(すべての例外を捕捉していない)点
サービスが 2 つのパラメータを持つ場合は、別の定義が必要
code:fsharp
let serviceExceptionAdapter serviceInfo serviceFn x y =
try
Ok (serviceFn x y)
with
| :? TimeoutException as ex ->
Error { Service = serviceInfo; Exception = ex }
| :? AuthorizationExcetion as ex ->
Error { Service = serviceInfo; Exception = ex }
ここまでのアダプタブロックは汎用的だが、場合によっては、特定のサービスにフォーカスしたものを用意したほうが良いこともある
e.g.
データベースから発生した例外を、以下のようなドメインに適したケース型を持つ 選択型 に変換する レコードが見つからないケース
キーが重複しているケース
...
呼び出し側の例
code:fsharp
let serviceInfo = {
Name = "AddressCheckingService"
Endpoint = ...
}
// 例外を発生させるサービス
let checkAddressExists = ...
// val checkAddressExists: UnvalidatedAddress -> Result<CheckedAddress, RemoteServiceError>
let checkAddressExists address =
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
adaptedService adress
1. PlaceOrderError を PlaceOrderError に追加する
code:fsharp
type PlaceOrderError =
| Validation of ValidationError
| Pricing of PricingError
| RemoteService of RemoteServiceError
2. mapError で変換する
code:fsharp
let checkAddressExist address =
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
address
|> adaptedService
|> Result.mapError
何も返さない関数
以下のように I/O に書き込んでいる関数は何も返さない code:fsharp
// val logError: msg: string -> unit
let logError msg = printfn "ERROR %s" msg
まず、受け取った入力を用いて何も返さない関数を呼び出し、元の入力を返す関数(パススルー関数: ここでは tee と呼ぶ)を実装する
https://scrapbox.io/files/66b064903b2e23001d3bb1f0.png
code:fsharp
// val tee: f: ('a -> unit) -> x: 'a -> 'a
let tee f x =
f x
x
そして、tee を適用した後、Result.map を適用することで 2 トラック関数に変換できる
code:fsharp
// val adaptDeadEnd: f: ('a -> unit) -> Result<'a, 'error>
let adaptDeadEnd f = Result.map (tee f)
https://scrapbox.io/files/66b06547fdd4fb001c445b0e.png